using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

using Derm.Render;

namespace ShaderGen.Ui
{
	sealed class ShaderTextBox : RichTextBox
	{
		#region Constructors

		static ShaderTextBox()
		{
			SyntaxHighlightContext syntaxContext;

			syntaxContext = new SyntaxHighlightContext(sGlsl120Keyworks, sGlsl120Proprocessor);
			sSyntaxContext[RenderContext.GLSLVersion.Version_1_2] = syntaxContext;

			syntaxContext = new SyntaxHighlightContext(sGlsl130Keyworks, sGlsl130Proprocessor);
			sSyntaxContext[RenderContext.GLSLVersion.Version_1_3] = syntaxContext;

			syntaxContext = new SyntaxHighlightContext(sGlsl140Keyworks, sGlsl130Proprocessor);
			sSyntaxContext[RenderContext.GLSLVersion.Version_1_4] = syntaxContext;

			syntaxContext = new SyntaxHighlightContext(sGlsl150Keyworks, sGlsl130Proprocessor);
			sSyntaxContext[RenderContext.GLSLVersion.Version_1_5] = syntaxContext;
		}

		public ShaderTextBox()
		{
			// General setup
			AcceptsTab = true;
			Multiline = true;
			TabStop = false;
			// Font used for display
			Font = new Font(FontFamily.GenericMonospace, 10.0f);
			// Syntax highlighting
			mCurrentSyntaxContext = sSyntaxContext[RenderContext.GLSLVersion.Version_1_5];

			TextChanged += ShaderTextBox_TextChanged;

			InitializeComponent();
		}

		#endregion

		#region Syntax Highlighing

		[DllImport("user32.dll")]
		public static extern bool LockWindowUpdate(IntPtr hWndLock);

		private class SyntaxHighlightContext
		{
			public SyntaxHighlightContext(string[] keywords, string[] preprocessor)
			{
				StringBuilder sb;

				// Regex for comments
				CommentRegex = new Regex(@"//.*$|/\*.*\*/", RegexOptions.Multiline);

				// Regex for keywords
				sb = new StringBuilder();
				sb.Append(@"^|\s+(");
				foreach (string keyword in keywords)
					sb.AppendFormat(@"{0}|", Regex.Escape(keyword));
				foreach (string preprocessorKeyword in preprocessor)
					sb.AppendFormat(@"#{0}|", Regex.Escape(preprocessorKeyword));
				sb.AppendFormat(@"__\w|");
				sb.Remove(sb.Length - 1, 1);
				sb.Append(@")\b|$");

				KeywordsRegex = new Regex(sb.ToString(), RegexOptions.Multiline);

				// Regex for preprocessor line
				sb = new StringBuilder();
				sb.Append(@"^(\s*)#(");
				foreach (string preprocessorKeyword in preprocessor)
					sb.AppendFormat(@"{0}|", Regex.Escape(preprocessorKeyword));
				sb.Remove(sb.Length - 1, 1);
				sb.Append(")");
				PreprocessorLineRegex = new Regex(sb.ToString(), RegexOptions.Multiline);
			}

			public Regex CommentRegex;

			public Regex KeywordsRegex;

			public Regex PreprocessorLineRegex;
		}

		public ShaderCompilerContext CurrentCompilerContext
		{
			get { return (mCurrentCompilerContext); }
		}

		private readonly ShaderCompilerContext mCurrentCompilerContext = new ShaderCompilerContext();

		public void HighlightSyntax()
		{
			// No syntax?
			if (mCurrentSyntaxContext == null)
				return;

            // Get the last cursor position in the text box
            int selPos = SelectionStart;
			int selLength = SelectionLength;

			try {
				LockWindowUpdate(Handle);

				// Grab information about the first visible line (for restoring vertical scroll)
				int firstLine = GetLineFromCharIndex(GetCharIndexFromPosition(new Point(0, 0))) + 1;
				int firstLinePos = GetFirstCharIndexFromLine(firstLine + 1);

				TextChanged -= ShaderTextBox_TextChanged;

				// Reset to all black
				SelectAll();
				SelectionColor = Color.Black;

				// GLSL keywords (blue)
				HighlightSyntax(mCurrentSyntaxContext.KeywordsRegex, Color.Blue);
				// GLSL comments (green)
				HighlightSyntax(mCurrentSyntaxContext.CommentRegex, Color.Green);

				// Grayout inactive code
				GraylightInactiveLines();

				// Restore vertical scroll
				SelectionStart = firstLinePos;

				// No color
				SelectionLength = 0;
				SelectionColor = Color.Black;

				// Restore caret position and selection length
				SelectionStart = selPos;
				SelectionLength = selLength;
			} finally {
				TextChanged += ShaderTextBox_TextChanged;
				LockWindowUpdate(IntPtr.Zero);
			}
		}

		/// <summary>
		/// Highlight syntax with a color.
		/// </summary>
		/// <param name="syntaxRegex">
		/// A <see cref="Regex"/> that specify what text shall be highlighted.
		/// </param>
		/// <param name="color">
		/// A <see cref="Color"/> that specify the highlight color.
		/// </param>
		private void HighlightSyntax(Regex syntaxRegex, Color color)
		{
			// Color matching text
            foreach (Match keyWordMatch in syntaxRegex.Matches(Text)) {
                Select(keyWordMatch.Index, keyWordMatch.Length);
                SelectionColor = color;
            }
		}

		/// <summary>
		/// 
		/// </summary>
		private void GraylightInactiveLines()
		{
			ShaderCompilerContext cctx = CurrentCompilerContext;
			uint inactiveNesting = 0, lineNumber = 0;
			bool active = true;

			if (cctx == null)
				return;
			
			foreach (string line in Lines) {
				Match directiveMatch = sPpConditionalRegex.Match(line);
				bool lastLine = false;

				if (directiveMatch.Success) {
					Group directiveGroup = directiveMatch.Groups["Directive"];
					string directive = directiveGroup.Value;

					if ((directive != "else") && (directive != "endif")) {
						if (directive != "elif")
							inactiveNesting++;

						if (active == true) {
							int conditionOffset = directiveGroup.Index + directiveGroup.Length;
							string condition = line.Substring(conditionOffset, line.Length - conditionOffset);

							// Evaluate preprocessor condition
							//active = cctx.EvaluatePreprocessorCondition(condition.Trim());
						}
					} else {
						if (directive == "else") {
							if (inactiveNesting == 1)
								active = !active;
						} else {
							if (--inactiveNesting == 0) {
								if (active == false)
									lastLine = true;
								active = true;
							}
						}
					}
				}

				// Graylight inactive lines
				if ((active == false) || (lastLine == true)) {
					int lineStart = GetFirstCharIndexFromLine((int)lineNumber);

					Select(lineStart, line.Length);
					SelectionColor = Color.Gray;
				}

				lineNumber++;
			}
		}

		/// <summary>
		/// 
		/// </summary>
		public bool GrayoutInactiveCode { get { return (mGrayoutInactiveCode); } set { mGrayoutInactiveCode = value; } }

		/// <summary>
		/// 
		/// </summary>
		private bool mGrayoutInactiveCode;

		/// <summary>
		/// Currently selected syntax context.
		/// </summary>
		private SyntaxHighlightContext mCurrentSyntaxContext;

		/// <summary>
		/// 
		/// </summary>
		private static readonly Dictionary<RenderContext.GLSLVersion, SyntaxHighlightContext> sSyntaxContext = new Dictionary<RenderContext.GLSLVersion,SyntaxHighlightContext>();

		private static readonly Regex sPpConditionalRegex = new Regex(@"# *(?<Directive>if|elif|ifdef|ifndef|else|endif).*", RegexOptions.Multiline | RegexOptions.Compiled);

		#region GLSL 1.20 Syntax

		private static readonly string[] sGlsl120Keyworks = new string[]
    	{
            "attribute", "const", "uniform", "varying",
            "centroid",
			"break", "continue", "do", "for", "while",
            "if", "else",
			"in", "out", "inout",
            "float", "int", "void", "bool", "true", "false",
            "invariant",
            "discard", "return",
            "mat2", "mat3", "mat4",
            "mat2x2", "mat2x3", "mat2x4",
			"mat3x2", "mat3x3", "mat3x4",
			"mat4x2", "mat4x3", "mat4x4",
            "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "bvec2", "bvec3", "bvec4",
            "sampler1D", "sampler2D", "sampler3D", "samplerCube",
            "sampler1DShadow", "sampler2DShadow",
            "struct",

            "defined"
    	};

		private static readonly string[] sGlsl120Proprocessor = new string[]
    	{
    		"define",
	        "undef",
			"if",
            "ifdef",
            "ifndef",
            "else",
            "elif",
            "endif",
            "error",
            "pragma",
            "extension",
            "version",
            "line",
            "include"           
    	};

		private static readonly string[] sGlsl120ProprocessorVars = new string[]
    	{
    		"__LINE__",
            "__FILE__",
			"__VERSION__"
    	};

		#endregion

		#region GLSL 1.30 Syntax

		private static readonly string[] sGlsl130Keyworks = new string[]
    	{
            "attribute", "const", "uniform", "varying",
            "centroid", "flat", "smooth", "noperspective",
			"break", "continue", "do", "for", "while", "switch", "case", "default",
            "if", "else",
			"in", "out", "inout",
            "float", "int", "void", "bool", "true", "false",
            "invariant",
            "discard", "return",
            "mat2", "mat3", "mat4",
            "mat2x2", "mat2x3", "mat2x4",
			"mat3x2", "mat3x3", "mat3x4",
			"mat4x2", "mat4x3", "mat4x4",
            "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "bvec2", "bvec3", "bvec4",
            "uint", "uvec2", "uvec3", "uvec4",
            "lowp", "mediump", "highp", "precision",
            "sampler1D", "sampler2D", "sampler3D", "samplerCube",
            "sampler1DShadow", "sampler2DShadow", "samplerCubeShadow",
            "sampler1DArray", "sampler2DArray",
			"sampler1DArrayShadow", "sampler2DArrayShadow",
			"isampler1D", "isampler2D", "isampler3D", "isamplerCube",
			"isampler1DArray", "isampler2DArray",
			"usampler1D", "usampler2D", "usampler3D", "usamplerCube",
			"usampler1DArray", "usampler2DArray",
            "struct",

            "defined"
    	};

		private static readonly string[] sGlsl130Proprocessor = new string[]
    	{
    		"define",
	        "undef",
			"if",
            "ifdef",
            "ifndef",
            "else",
            "elif",
            "endif",
            "error",
            "pragma",
            "extension",
            "version",
            "line",
            "include",
			"##"
    	};

		#endregion

		#region GLSL 1.40 Syntax

		private static readonly string[] sGlsl140Keyworks = new string[]
    	{
            "attribute", "const", "uniform", "varying",
            "layout",            
            "centroid", "flat", "smooth", "noperspective",
			"break", "continue", "do", "for", "while", "switch", "case", "default",
            "if", "else",
			"in", "out", "inout",
            "float", "int", "void", "bool", "true", "false",
            "invariant",
            "discard", "return",
            "mat2", "mat3", "mat4",
            "mat2x2", "mat2x3", "mat2x4",
			"mat3x2", "mat3x3", "mat3x4",
			"mat4x2", "mat4x3", "mat4x4",
            "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "bvec2", "bvec3", "bvec4",
            "uint", "uvec2", "uvec3", "uvec4",
            "lowp", "mediump", "highp", "precision",
            "sampler1D", "sampler2D", "sampler3D", "samplerCube",
            "sampler1DShadow", "sampler2DShadow", "samplerCubeShadow",
            "sampler1DArray", "sampler2DArray",
			"sampler1DArrayShadow", "sampler2DArrayShadow",
			"isampler1D", "isampler2D", "isampler3D", "isamplerCube",
			"isampler1DArray", "isampler2DArray",
			"usampler1D", "usampler2D", "usampler3D", "usamplerCube",
			"usampler1DArray", "usampler2DArray",
			"sampler2DRect", "sampler2DRectShadow", "isampler2DRect", "usampler2DRect",
            "samplerBuffer", "isamplerBuffer", "usampleBuffer",
            "struct",

            "defined"
    	};

		#endregion

		#region GLSL 1.50 Syntax

		private static readonly string[] sGlsl150Keyworks = new string[]
    	{
            "attribute", "const", "uniform", "varying",
            "layout",            
            "centroid", "flat", "smooth", "noperspective",
			"break", "continue", "do", "for", "while", "switch", "case", "default",
            "if", "else",
			"in", "out", "inout",
            "float", "int", "void", "bool", "true", "false",
            "invariant",
            "discard", "return",
            "mat2", "mat3", "mat4",
            "mat2x2", "mat2x3", "mat2x4",
			"mat3x2", "mat3x3", "mat3x4",
			"mat4x2", "mat4x3", "mat4x4",
            "vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "bvec2", "bvec3", "bvec4",
            "uint", "uvec2", "uvec3", "uvec4",
            "lowp", "mediump", "highp", "precision",
            "sampler1D", "sampler2D", "sampler3D", "samplerCube",
            "sampler1DShadow", "sampler2DShadow", "samplerCubeShadow",
            "sampler1DArray", "sampler2DArray",
			"sampler1DArrayShadow", "sampler2DArrayShadow",
			"isampler1D", "isampler2D", "isampler3D", "isamplerCube",
			"isampler1DArray", "isampler2DArray",
			"usampler1D", "usampler2D", "usampler3D", "usamplerCube",
			"usampler1DArray", "usampler2DArray",
			"sampler2DRect", "sampler2DRectShadow", "isampler2DRect", "usampler2DRect",
            "samplerBuffer", "isamplerBuffer", "usampleBuffer",
            "sampler2DMS", "isampler2DMS", "usampler2DMS",
			"sampler2DMSArray", "isampler2DMSArray", "usampler2DMSArray",
            "struct",

            "defined"
    	};

		#endregion

		#endregion

		#region Identation

		private uint GetCurrentLineIdentation(int offset)
		{
			string currentText = Text;
			int selStart = SelectionStart + offset;

			// Find the line start position in text
			while ((currentText[selStart] != '\n') && (currentText[selStart] != '\r'))
				selStart--;
			selStart++;

			// Detect the number of tabs of the current line	
			uint spaceCount = 0;

			while ((Char.IsWhiteSpace(currentText[selStart])) && (currentText[selStart] != '\n') && (currentText[selStart] != '\r')) {
				switch (currentText[selStart]) {
					case '\t':
						spaceCount += 4;
						break;
					default:
						spaceCount += 1;
						break;
				}
				selStart++;
			}

			return (spaceCount);
		}

		private uint GetSmartLineIdentation(int offset)
		{
			string currentText = Text.Substring(0, SelectionStart + offset);

			MatchCollection blockOpens = sCodeBlockOpenRegex.Matches(currentText);
			MatchCollection blockClose = sCodeBlockCloseRegex.Matches(currentText);
			int tabCount = blockOpens.Count - blockClose.Count;

			if (tabCount < 0) tabCount = 0;

			return ((uint)tabCount * 4);
		}

		private static readonly Regex sCodeBlockOpenRegex = new Regex(@"\{");

		private static readonly Regex sCodeBlockCloseRegex = new Regex(@"\}");

		#endregion

		#region Load & Save

		/// <summary>
		/// Load a shader text from file.
		/// </summary>
		/// <param name="path"></param>
		public void Load(string path)
		{
			if (path == null)
				throw new ArgumentNullException("path");

			// Load the shader text
			using (StreamReader sr = new StreamReader(path)) {
				Text = sr.ReadToEnd();
				HighlightSyntax();
			}

			// Store the path
			mShaderPath = path;
			// Reset modified flag
			mModified = false;
			if (OnTextModifiedChanged != null)
				OnTextModifiedChanged(this, EventArgs.Empty);
		}

		/// <summary>
		/// Save a shader text to file.
		/// </summary>
		public void Save()
		{
			// Save the shader text
			using (StreamWriter sw = new StreamWriter(mShaderPath, false)) {
				sw.Write(Text);
			}
			// Reset modified flag
			mModified = false;
			if (OnTextModifiedChanged != null)
				OnTextModifiedChanged(this, EventArgs.Empty);
		}

		/// <summary>
		/// Get a boolean value indicating whether the shader text was modified.
		/// </summary>
		public bool TextModified { get { return (mModified); } }

		/// <summary>
		/// Event raised whenever <see cref="TextModified"/> changes.
		/// </summary>
		public event EventHandler OnTextModifiedChanged;

		/// <summary>
		/// Path used for loading shader source.
		/// </summary>
		private string mShaderPath;

		/// <summary>
		/// Flag indicating whether the text was modified by last load/save.
		/// </summary>
		private bool mModified;

		#endregion

		#region Internal Event Handling

		protected override void OnKeyUp(KeyEventArgs e)
		{
			switch (e.KeyCode) {
				case Keys.Enter:
					{
						try {
							LockWindowUpdate(Handle);

							// Detect the number of spaces of the current line
							uint spaceCount = GetCurrentLineIdentation(-2);
							uint smartSpaceCount = GetSmartLineIdentation(0);

							if ((spaceCount == 0) || (spaceCount < smartSpaceCount))
								spaceCount = smartSpaceCount;

							// Process key press
							base.OnKeyUp(e);

							// Insert spaces on new line
							StringBuilder sb = new StringBuilder();
							uint spaceTabs = spaceCount / 4;

							spaceCount -= spaceTabs * 4;

							for (int i = 0; i < spaceTabs; i++)
								sb.Append("\t");
							for (int i = 0; i < spaceCount; i++)
								sb.Append(" ");

							mEnableHighlight = false;
							SelectedText = sb.ToString();
							mEnableHighlight = true;
						} finally {
							LockWindowUpdate(IntPtr.Zero);
						}
					}
					break;
				default:
					// Base implementation
					base.OnKeyUp(e);
					break;
			}
		}

		protected override void OnMouseUp(MouseEventArgs e)
		{
			base.OnMouseUp(e);
			if (e.Button == MouseButtons.Right) {
				CtxMenuShaderText.Show(this, e.Location);
			}
		}

		private void ShaderTextBox_TextChanged(object sender, EventArgs e)
		{
			// Highlight syntax
			if (mEnableHighlight == true)
				HighlightSyntax();
			// Reset modified flag
			mModified = true;
			if (OnTextModifiedChanged != null)
				OnTextModifiedChanged(this, EventArgs.Empty);
		}

		private bool mEnableHighlight = true;

		#endregion

		#region Composer Code

		private void InitializeComponent()
		{
			this.components = new System.ComponentModel.Container();
			this.CtxMenuShaderText = new System.Windows.Forms.ContextMenuStrip(this.components);
			this.MenuCutText = new System.Windows.Forms.ToolStripMenuItem();
			this.MenuCopyText = new System.Windows.Forms.ToolStripMenuItem();
			this.MenuPasteText = new System.Windows.Forms.ToolStripMenuItem();
			this.CtxMenuShaderText.SuspendLayout();
			this.SuspendLayout();
			// 
			// CtxMenuShaderText
			// 
			this.CtxMenuShaderText.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.MenuCutText,
            this.MenuCopyText,
            this.MenuPasteText});
			this.CtxMenuShaderText.Name = "CtxMenuShaderText";
			this.CtxMenuShaderText.Size = new System.Drawing.Size(181, 70);
			// 
			// MenuCutText
			// 
			this.MenuCutText.Name = "MenuCutText";
			this.MenuCutText.Size = new System.Drawing.Size(180, 22);
			this.MenuCutText.Text = "Cut";
			// 
			// MenuCopyText
			// 
			this.MenuCopyText.Name = "MenuCopyText";
			this.MenuCopyText.Size = new System.Drawing.Size(180, 22);
			this.MenuCopyText.Text = "toolStripMenuItem1";
			// 
			// MenuPasteText
			// 
			this.MenuPasteText.Name = "MenuPasteText";
			this.MenuPasteText.Size = new System.Drawing.Size(180, 22);
			this.MenuPasteText.Text = "toolStripMenuItem1";
			this.CtxMenuShaderText.ResumeLayout(false);
			this.ResumeLayout(false);

		}

		private ContextMenuStrip CtxMenuShaderText;

		private ToolStripMenuItem MenuCutText;
		private ToolStripMenuItem MenuCopyText;
		private ToolStripMenuItem MenuPasteText;

		private System.ComponentModel.IContainer components;

		#endregion
	}
}
